package com.bubblegumproject.ogo.action;

import com.bubblegumproject.ogo.component.Container;
import org.apache.log4j.Logger;

import java.util.HashMap;
import java.util.Map;

/**
 * A complete implementation of the {@link Agent} contract. This class provides a simple mechanism to map Action
 * classes to the Flow classes that will handle the given Action. It also provides access to a {@link Container} that
 * may be used to create and destroy Flow objects.
 * <p/>
 * Flow execution is done using a 'best effort' approach. The most appropriate
 * {@link com.bubblegumproject.ogo.component.ActionHandler} is selected from among the various handlers exposed by
 * the different Flow classes.
 * <p/>
 * XXX: This class must be optimized for multithreaded execution. Specifically interacting with the actionMap is
 * read-mostly and should be optimized for that.
 *
 * @author Azubuko Obele (buko.obele@gmail.com)
 */
public class SimpleAgent extends AbstractAgent {

    protected Container container;

    protected ActionHandlerInvoker actionHandlerInvoker;

    private Map<String, Class<? extends Flow>> actionMap = new HashMap<String, Class<? extends Flow>>();

    private Map<Source, Conversation> activeConversations = new HashMap<Source, Conversation>();

    private static final Logger log = Logger.getLogger(SimpleAgent.class);

    public Container getContainer() {
        return container;
    }

    public void setContainer(Container container) {
        this.container = container;
    }

    protected Context createContext() {
        // make the Container available for dependency injection
        Context context = super.createContext();
        context.set(container.getClass().getName(), container);
        return context;
    }

    public void stop() {
        super.stop();
        if (activeConversations.values().size() > 0) log.warn(this + " shutting down with active conversations!");
    }

    public void register(String scenarioId, Class<? extends Flow> flowClazz) {
        assert (scenarioId != null && flowClazz != null);
        if (actionMap.containsKey(scenarioId)) {
            throw new IllegalArgumentException(this + " already contains mapping for " + scenarioId);
        } else {
            actionMap.put(scenarioId, flowClazz);
            if (!container.contains(flowClazz)) container.register(flowClazz);
        }
    }

    public void register(Class<? extends Action> actionClazz, Class<? extends Flow> flowClazz) {
        register(actionClazz.getName(), flowClazz);
    }

    protected FlowExecution start(Action action) {
        assert (action != null);
        Conversation activeConversation = activeConversations.get(action.getSource());
        if (activeConversation != null) {
            return activeConversation;
        } else {
            Class<? extends Flow> flowClazz = find(action);
            return new SimpleFlowExecution(flowClazz, action);
        }
    }

    private Conversation start(SimpleFlowExecution sfe) {
        return new SimpleConversation(sfe.getAction(), this);
    }

    protected void stop(FlowExecution fe) {
        assert (fe != null);

        if (fe instanceof Conversation) {
            Conversation conversation = (Conversation) fe;
            activeConversations.remove(conversation.getSource());
            conversation.destroy();
            return;
        }

        SimpleFlowExecution sfe = (SimpleFlowExecution) fe;
        if (!sfe.getFlow().isFinished()) {
            Conversation conversation = start(sfe);
            activeConversations.put(sfe.getAction().getSource(), conversation);
        } else {
            sfe.destroy();
        }
    }

    protected Class<? extends Flow> find(Action action) {
        assert (action != null);
        // TODO: We need a better, more generic way to map actions to scenario IDs
        String scenarioId = null;
        if (action instanceof SignalAction) scenarioId = ((SignalAction) action).getName();
        else scenarioId = action.getClass().getName();
        
        Class<? extends Flow> flowClazz = actionMap.get(scenarioId);
        if (flowClazz != null) {
            return flowClazz;
        } else {
            throw new IllegalArgumentException(this + " has no Flow class registered for " + action);
        }
    }

    protected void execute(Flow flow, Action action) {
        assert (flow != null && action != null);

        if (actionHandlerInvoker == null) {
            actionHandlerInvoker = new SimpleActionHandlerInvoker(container);
        }

        boolean success = actionHandlerInvoker.invoke(action, flow);
        if (!success) log.warn(this + " failed to find ActionHandler to handle " + action);
    }

    private class SimpleFlowExecution implements FlowExecution {

        private Flow flow;

        private Action action;

        public SimpleFlowExecution(Class<? extends Flow> flowClazz, Action action) {
            // The Action Context will inherit the Agent Context and the Flow Context will inherit the Action Context
            // XXX: The problem here is that when the Container creates the Flow it will invoke its startup methods
            // but the Flow.setContext(...) method has not been set!
            // TODO: As a rule I think it makes a lot of sense to inject a Context into a Component BEFORE we invoke
            // any start methods. This is the only way to do meaningful configuration logic.
            // We need a new annotation @Contextualize which can be added to Component methods. These methods will
            // be passed the Context and invoked before a component is started. This lets the Component do any
            // configuration needed before its startup methods are invoked.
            action.inherit(SimpleAgent.this.getContext());
            this.flow = container.create(flowClazz, action.getContext());
            this.flow.setContext(new SimpleContext(action.getContext()));
            this.action = action;
        }

        public Flow getFlow() {
            return flow;
        }

        public Action getAction() {
            return action;
        }

        public void execute(Action a) {
            SimpleAgent.this.execute(flow, action);
        }

        public void destroy() {
            container.destroy(flow);
        }
    }
}
